// AquosDriver.cs
//
// Implements HomeUX.Components.AquosDriver.
//
// See DocFiles\AquosDriver.htm for driver usage information.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Linq;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Implements a HomeUX driver that controls a Sharp Aquos LC-52D65U, LC-42D65U, or LC-46D65U
/// LCD TV connected via a serial port.
/// </summary>
///
public class AquosDriver : Driver
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// Maps the name of each input specified on the .config file to the corresponding <r>Input</r>
	/// object.  Unused inputs are not included in this dictionary.
    /// </summary>
    Dictionary<string, Input> _mapNameToInput;

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    AquosHardware _hardware;

    /// <summary>
    /// The current "IsPowered" setting.
    /// </summary>
    bool _isPowered;

    /// <summary>
    /// The current "Input" setting.
    /// </summary>
    Input _input;

    /// <summary>
    /// The current "Volume" setting.
    /// </summary>
    double _volume;

    /// <summary>
    /// The current "IsMuted" setting.
    /// </summary>
    bool _isMuted;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>Driver.OnInitialize</r>.
    /// </summary>
    ///
    protected override void OnInitialize()
    {
        // read attributes of the "<Device>" XML element
        _serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");

        // initialize data structures
        _mapNameToInput = new Dictionary<string, Input>();

        // read attributes of child XML element
        foreach (ConfigurationElement element in Configuration.Children)
        {
            switch (element.Name)
            {

            case "Input":

                // set <input> to the "<Input>" element
                var input = new Input()
                {
                    Name = element.GetRequiredAttribute("Name"),
                    Id = element.GetRequiredIntAttribute("InputId"),
                };

                // ensure that <input.Name> hasn't already been taken by a previous "<Input>"
                // element, and add <input> to <_mapNameToInput>
                try
                {
                    _mapNameToInput.Add(input.Name, input);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate input name: {0}", input.Name);
                }
                break;

            }
        }

        // make sure there's at least one input
        if (_mapNameToInput.Count == 0)
        {
            var input = new Input()
            {
                Name = "Default",
                Id = 1
            };
        }

        // load previously-persisted state
        _isPowered = PersistedTree.TryFindValue("IsPowered", false);
        string inputName = PersistedTree.TryFindValue<string>("Input", null);
        if ((inputName == null) || !_mapNameToInput.TryGetValue(inputName, out _input))
            _input = _mapNameToInput.Values.First();
        _volume = PersistedTree.TryFindValue("Volume", 0.0);
        _isMuted = PersistedTree.TryFindValue("IsMuted", false);
    }

    /// <summary>
    /// Overrides <r>Driver.OnEnable</r>.
    /// </summary>
    ///
    protected override void OnEnable()
    {
        // enable occurs asynchronously -- queue an AquosEnableRequest to perform this operation
        QueueRequest(new AquosEnableRequest() { Driver = this }, false);
    }

    /// <summary>
    /// Overrides <r>Driver.OnDisable</r>.
    /// </summary>
    ///
    protected override void OnDisable()
    {
        // disable occurs synchronously -- perform it now
        CloseHardware();
        DisableCompleted();
    }

    /// <summary>
    /// Overrides <r>Driver.SerializeStateForClient</r>.
    /// </summary>
    ///
    /// <param name="sessionId">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    /// <param name="writer">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
		// write top-level nodes
		SerializeSyncNodeUpdate(writer, _isPowered, Name, "IsPowered");
        SerializeSyncNodeUpdate(writer, _input.Name, Name, "Input");
        SerializeSyncNodeUpdate(writer, _volume, Name, "Volume");
		SerializeSyncNodeUpdate(writer, _isMuted, Name, "IsMuted");

        // write each "<inputNamesPath>\input-name" node
        SyncPath inputNamesPath = SyncPath.Build("Configuration", Name, "InputNames");
        foreach (Input input in _mapNameToInput.Values)
            SerializeSyncNodeUpdate(writer, input.Name, inputNamesPath, input.Name);
    }

    /// <summary>
    /// See <r>Driver.ProcessSyncNodeUpdate</r>.
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="update">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        update.Path.VerifyPathLength(2);
        switch (update.Path.MatchRequiredPart(1, "IsPowered", "Input", "Volume", "IsMuted"))
        {

        // the order of these cases is the same as the order of strings in MatchRequiredPart above

        case 0: // "IsPowered"

            QueueRequest(new AquosIsPoweredChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewIsPowered = update.GetTypedValue<bool>()
            }, true);
            break;

        case 1: // "Input"

			string inputName = update.GetTypedValue<string>();
			Input input;
			if (!_mapNameToInput.TryGetValue(inputName, out input))
                throw new SyncException("Invalid input \"{0}\"", inputName);
            QueueRequest(new AquosInputChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewInput = input
            }, true);
            break;

        case 2: // "Volume"

            QueueRequest(new AquosVolumeChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewVolume = update.GetTypedValue<double>()
            }, true);
            break;

        case 3: // "IsMuted"

            QueueRequest(new AquosIsMutedChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewIsMuted = update.GetTypedValue<bool>()
            }, true);
            break;
        }
    }

    /// <summary>
    /// Overrides <r>Driver.MergeRequests</r>.
    /// </summary>
    ///
    /// <param name="futureRequests">See <r>Driver.MergeRequests</r>.</param>
    ///
    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
        // strategy: we only look at the initial set of AquosVolumeChangeRequest requests
        // (i.e. stopping at the first one that isn't such a request), and we look for cases
        // where request B is later (within this set) than request A, where A and B change
        // volume, and in that case we delete A since B overrides it
        while (true)
        {
            // we need at least 2 requests in order to do a merge
            if (futureRequests.Count < 2)
                return;

            // set <first> to the first request; return if it's not an AquosVolumeChangeRequest
			// request; note that we'll repeat this (since we're in a loop)
            var first = futureRequests[0] as AquosVolumeChangeRequest;
            if (first == null)
                return;

            // <first> is request A (in the comments above); set <other> to request B; if we find
            // B, remove A, but also add the originator (client session ID) of A to B (effectively
            // merging the requests); if we don't find B, stop looking for requests to merge (if
            // there are other requests that could be merged presumably they were already merged
            // during previous calls to ProcessSyncNodeUpdate)
            AquosVolumeChangeRequest other = FindRequestToMergeInto(futureRequests, first);
            if (other != null)
            {
                other.Originators.AddRange(first.Originators);
                futureRequests.RemoveAt(0);
            }
            else
                break;
        }
    }

    /// <summary>
    /// Assuming the first request in the list of future <r>Driver.Request</r> objects is a
    /// given <r>AquosVolumeChangeRequest</r>, this method scans whatever other
    /// <r>AquosVolumeChangeRequest</r> requests follow it (i.e. stopping at the first request
    /// that isn't <r>AquosVolumeChangeRequest</r>) to find the first such request and then
    /// returns that request (since it will override the first request).  If no such request is
    /// found, <n>null</n> is returned.
    /// </summary>
    ///
    /// <param name="futureRequests">The list of future <r>Driver.Request</r> objects, i.e.
    ///     requests that waiting to be processed.</param>
    ///
    /// <param name="first">The first request in <pr>futureRequests</pr>, which is an
    ///     <r>AquosVolumeChangeRequest</r></param>
    ///
    AquosVolumeChangeRequest FindRequestToMergeInto(IList<Driver.Request> futureRequests,
        AquosVolumeChangeRequest first)
    {
        for (int requestIndex = 1; requestIndex < futureRequests.Count; requestIndex++)
        {
            var other = futureRequests[requestIndex] as AquosVolumeChangeRequest;
            if (other == null)
                return null;
        }

        return null;
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Other Methods
	//

    /// <summary>
    /// Assuming <r>_hardware</r> is null, this method creates an <r>AquosHardware</r> object, used
    /// to access Aquos hardware, and sets <r>_hardware</r> to refer to it.
    /// </summary>
	///
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            // set <serialPort> to a new serial port, as specified by <_serialPortSpec>
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);

            // set <_hardware> to a newly-allocated AquosHardware object that will use <serialPort>
            // to communicate with the hardware
            _hardware = new AquosHardware(WorkQueue, serialPort, Name);

            // hook up <_hardware> events
            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandFailed += OnHardwareCommandFailed;
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            // open <_hardware>; this will fail if the serial port cannot be opened
            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to <_hardware>,
			// free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }

    }

    /// <summary>
    /// Assuming <r>_hardware</r> is not null, this method frees it.
    /// </summary>
    ///
    void CloseHardware()
    {
        // disconnect <_hardware> events
        _hardware.CommandFailed -= OnHardwareCommandFailed;
        _hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
        _hardware.WarningsReported -= OnHardwareWarningsReported;

		// dispose of <_hardware>
        _hardware.Dispose();
        _hardware = null;
    }

    /// <summary>
    /// See <r>AquosHardware.CommandFailed</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>AquosHardware.CommandFailed</r>.</param>
    ///
    /// <param name="e">See <r>AquosHardware.CommandFailed</r>.</param>
    ///
    public void OnHardwareCommandFailed(object sender, AquosHardwareCommandFailedEventArgs e)
    {
        // report the failure to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command failed", e.Command);
    }

    /// <summary>
    /// See <r>SimpleSerialHardware{}.CommandTimedOut</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    /// <param name="e">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    public void OnHardwareCommandTimedOut(object sender,
        SimpleSerialHardwareCommandTimedOutEventArgs e)
    {
        // report the timeout to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
    }

    /// <summary>
    /// See <r>AquosHardware.WarningsReported</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>AquosHardware.WarningsReported</r>.</param>
    ///
    /// <param name="e">See <r>AquosHardware.WarningsReported</r>.</param>
    ///
    public void OnHardwareWarningsReported(object sender, AquosHardwareWarningsReportedEventArgs e)
    {
        foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
    }

    /// <summary>
    /// Given a volume level (a <n>double</n>), this method returns the level limited to the
    /// range 0.0 to 1.0, inclusive.
    /// </summary>
    ///
    /// <param name="volume">The given volume level.</param>
    ///
    static double LimitVolume(double volume)
    {
        if (volume < 0)
            return 0;
        else
        if (volume > 1)
            return 1;
        else
            return volume;
    }

    /// <summary>
    /// Converts a driver volume level (0.0 to 1.0, inclusive) to a TV volume level (0 to 60),
	/// which is returned.
    /// </summary>
    ///
    /// <param name="volume">A volume level from 0.0 to 1.0, inclusive.</param>
    ///
    static int DriverVolumeToHardwareVolume(double volume)
    {
        return (int) (LimitVolume(volume) * 60);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // AquosRequest and Derived Types
    //

    /// <summary>
    /// A request from a client.
    /// </summary>
    ///
    abstract class AquosRequest : Driver.Request
    {
        /// <summary>
        /// The driver that owns this request.
        /// </summary>
        protected new AquosDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (AquosDriver)base.Driver;
            }
        }
    }

    /// <summary>
    /// A requst to enable the device.
    /// </summary>
    ///
    [DebuggerDisplay("Enable driver")]
    class AquosEnableRequest : AquosRequest, Driver.IEnableRequest
    {
        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
            try
            {
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end JdsRequest
            }

            // invoke AquosIsPoweredChangeRequest as a nested request
            var request = new AquosIsPoweredChangeRequest()
            {
                Driver = Driver,
                NewIsPowered = Driver._isPowered,
                OverrideOnExit = NextState
            };
            request.Start();
            yield return true;
            if (request.Error != null)
            {
                Error = request.Error;
                yield break;
            }
        }
    }

    /// <summary>
    /// A request to turn the TV on or off.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class AquosIsPoweredChangeRequest : AquosRequest
    {
        /// <summary>
        /// The new value for <r>Driver._isPowered</r>.
        /// </summary>
        public bool NewIsPowered;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // power the hardware on or off
            if (NewIsPowered)
            {
                // turn on the hardware
                var setIsPowered = new ResultHolder<GenericReply>(this);
                Driver.Trace(true, "HW: Power On", NewIsPowered);
                Driver._hardware.SetIsPowered(NewIsPowered, setIsPowered.OnDone);
                yield return true; // wait for async reply
                if (setIsPowered.Result == null)
                {
                    Error = "SetIsPowered failed";
                    yield break;
                }
                else
                    Driver._isPowered = NewIsPowered;

#warning this isn't working yet:
#if false
                // set the mute setting
                var setIsMuted = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set IsMuted={0} (due to Power On)", Driver._isMuted);
                Driver._hardware.SetIsMuted(Driver._isMuted, setIsMuted.OnDone);
                yield return true; // wait for async reply
                if (setIsMuted.Result == null)
                {
                    Error = "SetIsMuted failed during power-on";
                    yield break;
                }

                // set the volume, if we're not muted
                if (!Driver._isMuted)
                {
                    var setVolume = new ResultHolder<GenericReply>(this);
                    var hardwareVolume = DriverVolumeToHardwareVolume(Driver._volume);
					Driver.Trace(true, "HW: Set Volume={0} ({1}) (due to Power On)",
						hardwareVolume, Driver._volume);
                    Driver._hardware.SetVolume(hardwareVolume, setVolume.OnDone);
                    yield return true; // wait for async reply
                    if (setVolume.Result == null)
                    {
                        Error = "SetVolume failed during power-on";
                        yield break;
                    }
                }

                // set the input
                var setInput = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set Input={0} ({1}) (due to Power On)",
					Driver._input.Id, Driver._input.Name);
                Driver._hardware.SetInput(Driver._input.Id, setInput.OnDone);
                yield return true;
                if (setInput.Result == null)
                {
                    Error = "SetInput failed during power-on";
                    yield break;
                }
#endif
            }
            else
            {
                // turn off the hardware
                var setIsPowered = new ResultHolder<GenericReply>(this);
                Driver.Trace(true, "HW: Power Off", NewIsPowered);
                Driver._hardware.SetIsPowered(NewIsPowered, setIsPowered.OnDone);
                yield return true; // wait for async reply
                if (setIsPowered.Result == null)
                {
                    Error = "SetIsPowered failed";
                    yield break;
                }
                else
                    Driver._isPowered = NewIsPowered;
            }
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change IsPowered to {0}", NewIsPowered);
            }
        }
    }

    /// <summary>
    /// A request to change the TV's input.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class AquosInputChangeRequest : AquosRequest
    {
        /// <summary>
        /// The new value for <r>Driver._input</r>.
        /// </summary>
        public Input NewInput;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            var setInput = new ResultHolder<GenericReply>(this);
            Driver.Trace(true, "HW: Set Input={0}", NewInput.Id);
            Driver._hardware.SetInput(NewInput.Id, setInput.OnDone);
            yield return true; // wait for async reply
            if (setInput.Result == null)
            {
				Error = "SetInput failed";
                yield break;
            }

            // update driver state
            Driver._input = NewInput;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change Input to {0}", NewInput);
            }
        }
    }

    /// <summary>
    /// A request to change the volume of the TV.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
	class AquosVolumeChangeRequest : AquosRequest
	{
        /// <summary>
        /// The new volume level (0.0 to 1.0).
        /// </summary>
		public double NewVolume;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // set the hardware level to <NewVolume>; if the TV is muted, there's nothing to tell
			// the hardware at this point -- we'll use <NewVolume> when the TV is unmuted
            if (!Driver._isMuted)
            {
                var setVolume = new ResultHolder<GenericReply>(this);
                int tvVolume = DriverVolumeToHardwareVolume(NewVolume);
				Driver.Trace(true, "HW: Set Volume={0} ({1})", tvVolume, NewVolume);
                Driver._hardware.SetVolume(tvVolume, setVolume.OnDone);
                yield return true; // wait for async reply
                if (setVolume.Result == null)
                {
                    Error = "SetVolume failed";
                    yield break;
                }
            }

            // update driver state
            Driver._volume = NewVolume;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change volume to {0}", NewVolume);
            }
        }
    }

    /// <summary>
    /// A request to mute or un-mute the TV.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class AquosIsMutedChangeRequest : AquosRequest
    {
        /// <summary>
        /// The new value for <r>Driver._isMuted</r>.
        /// </summary>
        public bool NewIsMuted;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            var setIsMuted = new ResultHolder<GenericReply>(this);
            Driver.Trace(true, "HW: Set IsMuted={0}", NewIsMuted);
            Driver._hardware.SetIsMuted(NewIsMuted, setIsMuted.OnDone);
            yield return true; // wait for async reply
            if (setIsMuted.Result == null)
            {
				Error = "SetIsMuted failed";
                yield break;
            }

            // update driver state
            Driver._isMuted = NewIsMuted;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change IsMuted to {0}", NewIsMuted);
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Other Nested Types
    //

    /// <summary>
    /// Represents an input to the TV.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class Input
    {
        /// <summary>
        /// The name of this input.
        /// </summary>
        public string Name;

        /// <summary>
        /// The 1-based hardware input number of this input.
        /// </summary>
        public int Id;

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Input \"{0}\" (#{1})", Name, Id);
            }
        }
    }
}

}
